import { Player } from '@/common/Player'
import { vars } from './vars'
import store from '@/apps/panel/store'
import { isFirefox } from '@/common/web_extension'
import { interpreter } from './interpreter'
import log from '@/common/log'
import * as CONSTANT from '../common/constant'
import { monitor } from './monitor'
import { message } from 'antd'
import { END_REASON, PLAY_MODE } from '@/types/player'
import { callStack } from './callStack'
import { MacroInspector } from '@/types/monitor'
import { MacroResultStatus } from '@/types/macro_extra_data'
import { milliSecondsToStringInSecond } from '@/common/ts_utils'
import { panelUpdateBadge } from '@/common/badge'
import * as act from '@/apps/panel/actions'
import { getPlayTab } from '@/common/tab'
import { objMap } from '@/common/utils'
import clipboard from '@/common/clipboard'
import { updateIn, } from '@/common/utils'
import { replaceEscapedChar, } from './helper'
import { getStorageManager } from '@/file_storage'
import Ext from '@/common/web_extension'
import { askBgToRun } from './run_command'
import { askBg } from '@/common/ipc/ipc';

let onErrorCommand: any = null

function isPausedOrStopped(str: string) {
    return /player: paused or stopped/.test(str)
}

const REPLAY_SPEED_DELAY: any = {
    NODISPLAYV1: 1,
    NODISPLAY: 1,
    FASTV1: 1, // avoid UI freezing (DemoCsvReadArray: Fast=0 is ~30-40% faster as no UI updates)
    FAST: 1,
    MEDIUMV1: 300,
    MEDIUM: 300,
    SLOWV1: 2000,
    SLOW: 2000
}

const checkRelativeTabId = (tabId: number) => {
    return new Promise((resolve, reject) => {
        Ext.tabs.get(tabId)
            .then((tab: any) => {
                if (tab.length !== 0) {
                    resolve(true);
                } else { resolve(false); }
            }).catch(() => {
                resolve(false);
            })
    })
}

const getTabIdwithIndex0 = () => {
    return new Promise((resolve, reject) => {
        Ext.tabs.query({}, function (tabs: any) {
            const ctab = tabs.filter((r: any) => r.active === true && r.url.indexOf('chrome-extension://') == -1) // TODO: add "moz-extension://" too ??
            resolve(ctab[0])
        });
    })
}

/**
 * 播放器
 * 用例的播放流程，由nextIndex控制具体的执行顺序
 * 
 */
export const player = new Player({
    // player的准备
    prepare: (state: any) => {
        // Each 'replay' has an independent variable scope,
        // with global variables as initial scope
        // 变量作用域
        if (state.extra.isBottomFrame && !state.extra.isBackFromCalling) {
            if (state.keepVariables !== 'yes') {
                // reset，删除运行时临时变量，只保留全局变量
                vars.reset({ keepGlobal: true })
            }
            vars.set(state.public.scope || {}, true)
            vars.set({
                '!StatusOK': true,
                '!WaitForVisible': false,
                '!StringEscape': true,
                '!IMAGEX': 0,
                '!IMAGEY': 0,
                '!OCRX': 0,
                '!OCRY': 0,
                '!OCRHEIGHT': 0,
                '!OCRWIDTH': 0,
                '!AI1': 0,
                '!AI2': 0,
                '!AI3': 0,
                '!AI4': 0,
                '!LAST_DOWNLOADED_FILE_NAME': vars.get('!LAST_DOWNLOADED_FILE_NAME') || '',
                '!URL': state.playUrl || '',
                '!CURRENT_TAB_NUMBER': state.playtabIndex,
                '!CURRENT_TAB_NUMBER_RELATIVE': 0,
                '!CURRENT_TAB_NUMBER_RELATIVE_INDEX': state.playtabIndex,
                '!CURRENT_TAB_NUMBER_RELATIVE_ID': state.playtabId,
                '!OCRENGINE': store.getState().config.ocrEngine,
                '!OCRLANGUAGE': store.getState().config.ocrLanguage,
                '!BROWSER': isFirefox() ? 'firefox' : 'chrome',
                '!OS': (() => {
                    const ua = window.navigator.userAgent
                    if (/windows/i.test(ua)) return 'windows'
                    if (/mac/i.test(ua)) return 'mac'
                    return 'linux'
                })()
            }, true)
        }

        // 非调用返回
        if (!state.extra.isBackFromCalling) {
            interpreter.reset()
            interpreter.preprocess(state.resources)
        }
        //  请求bg
        return askBg('PANEL_START_PLAYING', {
            url: state.startUrl,
            shouldNotActivateTab: true
        })
    },
    // run之前的准备
    beforeRun: (command: any, state: any) => {

        // ** commands are by default superfast except it has any !REPLAYSPEED command in lower index that has v1 as suffix like: fastv1
        const superFast = /^(nodisplay|fast|medium|slow)$/i.test(vars.get('!REPLAYSPEED')) ? true : false;
        const firstOnDownloadIndex = state.resources.findIndex((command: any) => command.cmd === 'onDownload')
        let hasOnDownloadCmd = false;// 是否有下载命令
        if (firstOnDownloadIndex !== -1) {
            const nextIndex = state.nextIndex
            if (firstOnDownloadIndex < nextIndex) {
                hasOnDownloadCmd = true
            }
        }

        const useClipboard = /!clipboard/i.test(command.target + ';' + command.value)

        if (isFirefox()) {
            switch (command.cmd) {
                case 'onDownload':
                    store.dispatch(act.addLog('warning', 'onDownload - changing file names not supported by Firefox extension api yet'))
                    break
            }
        }

        // Set clipboard variable if it is used
        if (useClipboard) {
            vars.set({ '!CLIPBOARD': clipboard.get() })
        }

        if (state.extra.isBottomFrame) {
            vars.set(
                {
                    '!LOOP': state.loopsCursor
                },
                true
            )
        }

        vars.set(
            {
                '!RUNTIME': milliSecondsToStringInSecond(
                    monitor.getDataFromInspector(callStack.bottom().id, MacroInspector.LoopTimer)
                )
            },
            true
        )

        if (command.cmd === 'open' || command.cmd === 'openBrowser') {
            // const indexR = vars.get('!CURRENT_TAB_NUMBER_RELATIVE') + 1;
            // vars.set({ '!CURRENT_TAB_NUMBER_RELATIVE': indexR}, true)
            command = { ...command, href: state.startUrl }
        }

        // Note: translate shorthand '#efp'
        if (command.target && /^#efp$/i.test(command.target.trim())) {
            // eslint-disable-next-line no-template-curly-in-string
            command.target = '#elementfrompoint (${!imageX}, ${!imageY})'
        }

        const isRelatedToExecuteScript =
            [
                'executeScript',
                'executeScript_Sandbox',
                'executeAsyncScript',
                'executeAsyncScript_Sandbox',
                'if',
                'while',
                'gotoIf',
                'elseif',
                'repeatIf'
            ].indexOf(command.cmd) !== -1

        if (command.cmd !== 'comment') {
            // Replace variables in 'target' and 'value' of commands
            ['target', 'value'].forEach((field) => {
                if (command[field] === undefined) return
                const oldEval =
                    (command.cmd === 'storeEval' && field === 'target') ||
                    (command.cmd === 'gotoIfxxx' && field === 'target') ||
                    (command.cmd === 'ifxxx' && field === 'target') ||
                    (command.cmd === 'whilexxx' && field === 'target')
                const opts: any = oldEval ? { withHashNotation: true } : {}

                opts.shouldStringify = oldEval || isRelatedToExecuteScript

                command = {
                    ...command,
                    [field]: vars.render(
                        replaceEscapedChar(
                            ['type'].includes(command.cmd) ? command[field] : command[field].trim(),
                            command,
                            field,
                            vars.get('!StringEscape')
                        ),
                        opts
                    )
                }
            })
        }

        // add timeout info to each command's extra
        // Please note that we must set the timeout info at runtime for each command,
        // so that timeout could be modified by some 'store' commands and affect
        // the rest of commands
        command = updateIn(
            ['extra'],
            (extra: any) => ({
                ...(extra || {}),
                timeoutPageLoad: vars.get('!TIMEOUT_PAGELOAD'),
                timeoutElement: vars.get('!TIMEOUT_WAIT'),
                timeoutDownload: vars.get('!TIMEOUT_DOWNLOAD'),
                timeoutDownloadStart: vars.get('!TIMEOUT_DOWNLOAD_START') || Math.max(10, vars.get('!TIMEOUT_WAIT')),
                lastCommandOk: vars.get('!LASTCOMMANDOK'),
                errorIgnore: !!vars.get('!ERRORIGNORE'),
                waitForVisible: !!vars.get('!WAITFORVISIBLE'),
                superFast: superFast,
                hasOnDownloadCmd: hasOnDownloadCmd
            }),
            command
        )
        console.log('beforeRun :>> 命令准备运行', command, state)
        return {
            command,
            state
        }
    },
    run: (command: any, state: any) => {
        return new Promise((resolve, reject) => {
            interpreter.run(command, state.nextIndex)
                .then(async (result) => {
                    const { byPass, isFlowLogic, nextIndex } = result

                    if (result.tabIndex !== undefined) {
                        // 处理切换页签
                        const isFoundTab = await checkRelativeTabId(vars.get('!CURRENT_TAB_NUMBER_RELATIVE_ID'));
                        let CURRENT_TAB_NUMBER_RELATIVE_INDEX = isFoundTab ? vars.get('!CURRENT_TAB_NUMBER_RELATIVE_INDEX') : 'NA';

                        if (state.mode == 'LOOP' && state.nextIndex == 1) {
                            CURRENT_TAB_NUMBER_RELATIVE_INDEX = 0;
                        }
                        if (CURRENT_TAB_NUMBER_RELATIVE_INDEX === 'NA') {
                            let tabF: any = await getTabIdwithIndex0();
                            CURRENT_TAB_NUMBER_RELATIVE_INDEX = tabF['index'] !== undefined ? tabF['index'] : 0;
                            vars.set({ '!CURRENT_TAB_NUMBER_RELATIVE_ID': tabF['id'] }, true)
                            vars.set({ '!CURRENT_TAB_NUMBER_RELATIVE_INDEX': tabF['index'] }, true)
                        }
                        const indexR = result.tabIndex - CURRENT_TAB_NUMBER_RELATIVE_INDEX;
                        vars.set({ '!CURRENT_TAB_NUMBER_RELATIVE': indexR }, true)
                        vars.set({ '!CURRENT_TAB_NUMBER': result.tabIndex }, true)
                    }

                    // Record onError command
                    if (command.cmd === 'onError') {
                        onErrorCommand = command
                    }

                    // fix sandbox array issue
                    // 应该放到interpreter中
                    if (command.cmd == 'executeScript_Sandbox') {
                        // console.log('executeScript_Sandbox result:>>', result)

                        let resultVars = result.vars
                        if (!resultVars) {
                            throw new Error('E503: A variable is required in the value field.')
                        }

                        let varKey = Object.keys(resultVars)[0] // always the first key
                        let varValue = Object.values(resultVars)[0] // always the first value
                        console.log('varKey:>> ', varKey)
                        console.log('varValue:>> ', varValue)

                        const convertObjFormatToRealObj: any = (obj: any) => {
                            if (!obj) return obj
                            // console.log('obj:>> ', obj)
                            let newObj: any
                            if (obj?.properties) {
                                let varValueType = obj.class == "Array" ? 'array' : 'object'
                                console.log('varValueType:>> ', varValueType)
                                if (varValueType == 'array') {
                                    // it's an array
                                    newObj = []
                                    for (let i = 0; i < obj.properties.length; i++) {
                                        newObj.push(convertObjFormatToRealObj(obj.properties[i]))
                                    }
                                } else {
                                    // it's an object
                                    newObj = {}
                                    newObj = convertObjFormatToRealObj(obj.properties)
                                }
                            } else {
                                // console.log('last obj:>> ', obj)
                                let newKeys = Object.keys(obj)
                                let values = Object.values(obj)
                                let hasObjectOrArray = values.some((val: any) => val?.properties)
                                if (hasObjectOrArray) {
                                    newObj = {}
                                    for (let i = 0; i < newKeys.length; i++) {
                                        newObj[newKeys[i]] = convertObjFormatToRealObj(values[i])
                                    }
                                } else {
                                    newObj = obj
                                }
                            }
                            return newObj
                        }

                        let finalValue = convertObjFormatToRealObj(varValue)
                        // console.log('finalValue:>> ', finalValue)

                        // put explicitly
                        const result_ = {
                            byPass,
                            vars: {
                                [varKey]: finalValue
                            }
                        }

                        return Promise.resolve(result_)
                    }

                    if (byPass) return Promise.resolve(result)

                    if (isFlowLogic) return Promise.resolve({ nextIndex })

                    return askBgToRun(command)
                })
                .then(resolve, reject)
        }).catch(e => {
            // Note: it will just log errors instead of a stop of whole macro, in following situations
            // 1. variable !ERRORIGNORE is set to true
            // 2. There is an `onError` command ahead in current loop.
            // 3. it's in loop mode, and it's not the last loop, and onErrorInLoop is continue_next_loop,
            if (vars.get('!ERRORIGNORE')) {
                return {
                    log: {
                        error: e.message
                    }
                }
            }

            if (onErrorCommand) {
                const value = onErrorCommand.value && onErrorCommand.value.trim()
                const target = onErrorCommand.target && onErrorCommand.target.trim()

                if (/^#restart$/i.test(target)) {
                    store.dispatch(act.addLog('status', 'onError - about to restart'))

                    e.restart = true
                    throw e
                } else if (/^#goto$/i.test(target)) {
                    store.dispatch(act.addLog('status', `onError - about to goto label '${value}'`))

                    return Promise.resolve({
                        log: {
                            error: e.message
                        },
                        nextIndex: interpreter.commandIndexByLabel(value)
                    })
                }
            }

            const isPausedStopped = isPausedOrStopped(e.message)
            const continueNextLoop = state.mode === PLAY_MODE.LOOP &&
                state.loopsCursor < state.loopsEnd &&
                store.getState().config.onErrorInLoop === 'continue_next_loop'

            if (continueNextLoop) {
                if (isPausedStopped) {
                    return {
                        // Note: simply set nextIndex to command count, it will enter next loop
                        nextIndex: state.resources.length
                    }
                }

                return {
                    log: {
                        error: e.message
                    },
                    // Note: simply set nextIndex to command count, it will enter next loop
                    nextIndex: state.resources.length
                }
            }

            // Note: set these status values to false
            // status of those logs above will be taken care of by `handleResult`
            vars.set({
                '!LastCommandOK': false,
                '!StatusOK': false
            }, true)

            throw e
        })
    },
    // run之后处理结果
    handleResult: (result: { pageUrl?: string; vars?: any; log?: any; screenshot?: any; control?: any; nextIndex?: number }, command: any, state: any) => {

        console.log('handleResult :>> 命令处理结果', result, command,)
        const prepares = []

        // 当前的tabIndex
        getPlayTab().then((tab: any) => {
            vars.set({ '!CURRENT_TAB_NUMBER': tab.index }, true)
        })

        // Every command should return its window.url
        if (result && result.pageUrl) {
            vars.set({ '!URL': result.pageUrl }, true)
        }

        // 命令设置变量
        if (result && result.vars) {
            const newVars = objMap(val => {
                if (val && val.__undefined__) return undefined
                return val
            }, result.vars)

            log('set vars', newVars)

            try {
                vars.set(newVars)

                // Note: if set value to !Clipboard, there is an async job we must get done before handleResult could return
                const clipBoardKey = Object.keys(result.vars).find(key => /!clipboard/i.test(key))
                if (clipBoardKey) {
                    prepares.push(
                        Promise.resolve(clipboard.set(result.vars[clipBoardKey]))
                    )
                }

                // Note: if user sets !timeout_macro to some other value, re-calculate the time left
                const timeoutMacroKey = Object.keys(result.vars).find(key => /!timeout_macro/i.test(key))

                if (timeoutMacroKey) {
                    const frameId = callStack.peek().id
                    monitor.restartInspector(frameId, MacroInspector.Countdown)
                }
            } catch (e) {
                if (newVars['!ocrlanguage']) {
                    let ocrEngine = vars.get('!ocrEngine') || store.getState().config.ocrEngine
                    // 99 98 1 2 
                    let ocrEngineName = ocrEngine == 99 ? 'XModule' : ocrEngine == 98 ? 'JavaScript OCR' : (ocrEngine == 1 || ocrEngine == 2) ? 'OCR.Space Engine' : 'OCR Engine'

                    return Promise.reject(new Error(`E502: ${ocrEngineName} encountered a problem`))
                }
                return Promise.reject(e)
            }
        }

        let hasError = false

        // 命令打印日志
        if (result && result.log) {
            if (result.log.info) {
                store.dispatch(act.addLog('echo', '打印：' + result.log.info, result.log.options))

                if (result.log.options && result.log.options.notification) {
                    askBg('PANEL_NOTIFY_ECHO', { text: result.log.info })
                }
            }

            if (result.log.warning) {
                store.dispatch(act.addLog('warning', result.log.warning, result.log.options))
            }

            if (result.log.error && !isPausedOrStopped(result.log.error)) {
                store.dispatch(act.addPlayerWarningCommandIndex(command.id))
                store.dispatch(act.addLog('error', result.log.error, { ignored: true }))
                hasError = true
            }
        }

        // From spec: !StatusOK, very similar to !LastCommandOK but it does not get reset by a “good” command.
        // If set to error, it remains like this. But a user can use store | true | !StatusOK to manually reset it.
        if (command.cmd !== 'echo') {
            vars.set({ '!LastCommandOK': !hasError }, true)
        }

        if (hasError) {
            vars.set({ '!StatusOK': false }, true)
        }

        // 通过命令截图
        if (result && result.screenshot) {
            store.dispatch(act.addLog('info', 'a new screenshot captured'))

            getStorageManager()
                .getScreenshotStorage()
                .getLink(result.screenshot.name)
                .then(link => ({
                    ...result.screenshot,
                    url: link
                }))
                .then(() => {
                    store.dispatch(act.listScreenshots())
                })
                .catch(e => {
                    log.error('screenshot obj error 1', e)
                    log.error('screenshot obj error stack', e.stack)
                })
        }

        //  通过命令暂停
        if (result && result.control) {
            switch (result.control.type) {
                case 'pause':
                    // Important: should only pause test case player, not test suite player
                    // Because once test suite player is paused, it is supposed to run the test case from start again
                    askBg('PANEL_NOTIFY_AUTO_PAUSE', {})

                    // pause() returns a promise that doesn't resolve,
                    // must return that promise here to pause any further execution
                    return player.pause()
                default:
                    throw new Error(`Control type '${result.control.type}' not supported yet`)
            }
        }

        // 运行速度
        if (/^(nodisplay|fast|medium|slow)$/i.test(vars.get('!REPLAYSPEED'))) {
            player.setSuperFastMode(true)
        } else {
            player.setSuperFastMode(false)
        }

        // 覆盖运行速度
        if (store.getState().replaySpeedOverrideToFastMode &&
            (state.postDelay !== REPLAY_SPEED_DELAY['FAST'] || store.getState().noDisplayInPlay)) {
            store.dispatch(act.setNoDisplayInPlay(false))
            player.setPostDelay(REPLAY_SPEED_DELAY['FAST'])

        } else {

            // 设置运行速度
            if (/^(fastv1|fast|mediumv1|medium|slowv1|slow|nodisplayv1|nodisplay)$/i.test(vars.get('!REPLAYSPEED'))) {
                const val = vars.get('!REPLAYSPEED').toUpperCase();
                player.setPostDelay(REPLAY_SPEED_DELAY[val])
            }

            const replaySpeedKey = Object.keys(result?.vars || {}).find(key => key.toUpperCase() === '!REPLAYSPEED')

            // Save nodisplay to store to reflect it in rendering
            // if !REPLAYSPEED is updated in vars
            if (replaySpeedKey) {
                store.dispatch(act.setNoDisplayInPlay(/^nodisplayv1$/i.test(vars.get('!REPLAYSPEED')) || /^nodisplay$/i.test(vars.get('!REPLAYSPEED'))))
            }
        }

        // For those flow logic that set nextIndex directly in Interpreter.run method
        if (result && result.nextIndex !== undefined) {
            return Promise.all(prepares).then(() => result.nextIndex)
        }

        // For those flow logic that has to get result from bg
        // and return nextIndex in Interpreter.postRun
        return Promise.all(prepares)
            .then(() => interpreter.postRun(command, state.nextIndex, result))
            .then((data = {}) => data.nextIndex)
    }
}, {
    preDelay: 0
})

// 播放器监听事件
player.on('BREAKPOINT', () => {
    askBg('PANEL_NOTIFY_BREAKPOINT', {})
})

player.on('LOOP_START', ({ loopsCursor, extra }: any) => {
    if (extra.isBottomFrame) {
        // Note: set 'csv read line number' to loops whenever a new loop starts
        vars.set({
            '!CsvReadLineNumber': loopsCursor,
            '!visualSearchArea': 'viewport',
            '!StatusOK': true
        }, true)
    }

    const { frameId } = extra

    // Note: reset macro timeout, and loop timer on each loop
    monitor.restartInspector(frameId, MacroInspector.LoopTimer)
    monitor.restartInspector(frameId, MacroInspector.Countdown)

    if (extra.isBottomFrame) {
        // Note: reset onErrorCommand on each loop
        onErrorCommand = null
    }
})

player.on('LOOP_RESTART', ({ currentLoop, loopsCursor }: any) => {

    askBg('PANEL_STOP_PLAYING', {})
    askBg('PANEL_START_PLAYING', {
        shouldNotActivateTab: true
    })

    store.dispatch(act.addLog('status', `Current loop: ${currentLoop}`))
})

// 开始
player.on('START', ({ title, extra }: any) => {
    log('START')
    store.dispatch(act.startPlaying());// 修改状态
    store.dispatch(act.setPlayerState({
        status: CONSTANT.PLAYER_STATUS.PLAYING, // 设置player的状态
        nextCommandIndex: null,
    }))

    if (!extra.isBackFromCalling) {
        // 重置MacroId的执行结果
        store.dispatch(act.updateMacroExtra(
            store.getState().editor.editing.meta.src!.id,
            {
                doneCommandIndices: [],
                errorCommandIndices: [],
                warningCommandIndices: []
            }
        ))
    }

    store.dispatch(act.addLog('status', `Playing macro ${title}`))

})

player.on('PREPARED', ({ extra }: Record<string, any>) => {
    if (!extra.isBackFromCalling) {
        // PREPARED event means all variables are already set
        const { frameId } = extra
        monitor.addTarget(frameId)
    }
})

player.on('PAUSED', () => {
    log('PAUSED')
    store.dispatch(act.setPlayerState({
        status: CONSTANT.PLAYER_STATUS.PAUSED
    }))

    store.dispatch(act.addLog('status', `Macro paused`))

    // Pause all monitors (timers, coundown)
    monitor.pause()
})

player.on('RESUMED', () => {
    log('RESUMED')
    store.dispatch(act.setPlayerState({
        status: CONSTANT.PLAYER_STATUS.PLAYING
    }))

    store.dispatch(act.addLog('status', `Macro resumed`))

    // Resume all monitors (timers, coundown)
    monitor.resume()
})

player.on('END', (obj: Record<string, any>) => {
    log('END', obj)

    askBg('PANEL_STOP_PLAYING', {})

    store.dispatch(act.stopPlaying())

    const state = store.getState()
    const extraState = state.player.nextCommandIndex !== null ? { lastNextCommandSeq: state.player.nextCommandIndex } : {}

    store.dispatch(act.setPlayerState({
        status: CONSTANT.PLAYER_STATUS.STOPPED,
        stopReason: obj.reason,
        nextCommandIndex: null,
        timeoutStatus: null,
        ...extraState
    }))

    if (vars.get('!PROXY_EXEC_COUNT') > 0 && store.getState().config.turnOffProxyAfterReplay) {
        askBg('PANEL_SET_PROXY', { proxy: null })
        store.dispatch(act.addLog('info', 'Proxy reset to none'))
    }

    const tcId = obj.extra && obj.extra.id

    switch (obj.reason) {
        case END_REASON.COMPLETE:
            if (tcId) store.dispatch(act.updateMacroPlayStatus(tcId, MacroResultStatus.SUCCESS))
            message.success('Macro completed running', 1.5)
            break

        case END_REASON.ERROR:
            const stacks = callStack.toArray();
            const len = stacks.length;

            stacks.forEach((item, i) => {
                const status = i === len - 1 ? MacroResultStatus.ERROR : MacroResultStatus.ERROR_IN_SUB
                store.dispatch(act.updateMacroPlayStatus(item.resource.id, status))
            });

            message.error('Macro encountered some error', 1.5)
            break
    }

    const logMsg = {
        [END_REASON.COMPLETE]: 'Macro completed',
        [END_REASON.ERROR]: 'Macro failed',
        [END_REASON.MANUAL]: 'Macro was stopped manually'
    }

    const { frameId } = obj.extra
    const ms = monitor.getDataFromInspector(frameId, MacroInspector.Timer)

    store.dispatch(act.
        addLog(
            'info',
            logMsg[obj.reason as END_REASON] + ` (Runtime ${milliSecondsToStringInSecond(ms)})`
        )
    )

    monitor.stopInspector(frameId, MacroInspector.Timer)
    monitor.stopInspector(frameId, MacroInspector.LoopTimer)
    monitor.stopInspector(frameId, MacroInspector.Countdown)

    // Note: show in badage the play result
    if (obj.reason === END_REASON.COMPLETE ||
        obj.reason === END_REASON.ERROR) {

        panelUpdateBadge({
            type: 'play',
            blink: 5000,
            text: obj.reason === END_REASON.COMPLETE ? 'done' : 'err',
            ...(obj.reason === END_REASON.COMPLETE ? {} : { color: 'orange' })
        })
    }

    if (store.getState().player.mode !== CONSTANT.PLAYER_MODE.TEST_SUITE) {
        store.dispatch(act.updateUI({ shouldEnableDesktopAutomation: undefined }))
    }

})

player.on('TO_PLAY', ({ index, currentLoop, loops, resource, extra }: Record<string, any>) => {
    log('TO_PLAY', index, resource, 'currentLoop', currentLoop)
    if (resource.cmd === 'end') {
        return
    }
    store.dispatch(act.setPlayerState({
        timeoutStatus: null,
        nextCommandIndex: index,
        ...(extra.isBottomFrame ? {
            currentLoop,
            loops
        } : {})
    }))

    const triple = [resource.cmd, resource.target, resource.value]
    const str = ['', ...triple, ''].join(' | ')
    store.dispatch(act.addLog('reflect', `执行: ${str}`))

    // Note: show in badage the current command index (start from 1)
    panelUpdateBadge({
        type: 'play',
        text: '' + (index + 1)
    })
})

player.on('PLAYED_LIST', ({ indices }: Record<string, any>) => {
    log('PLAYED_LIST', indices)
    store.dispatch(act.updateMacroDoneCommandsIndices(
        store.getState().editor.editing.meta.src?.id || '',
        indices
    ))
})

player.on('ERROR', ({ errorIndex, msg, stack, restart }: Record<string, any>) => {
    log.error(`command index: ${errorIndex}, Error: ${msg}, Stack: ${stack}`)
    store.dispatch(act.addPlayerErrorCommandIndex(errorIndex))
    store.dispatch(act.addLog('error', msg))

    // Note: restart this player if restart is set to true in error, and it's not in test suite mode
    // Delay the execution so that 'END' event is emitted, and player is in stopped state
    if (restart && store.getState().player.mode === CONSTANT.PLAYER_MODE.TEST_CASE) {
        setTimeout(() => player.replayLastConfig(), 50)
    }
})

player.on('DELAY', ({ total, past }: Record<string, any>) => {
    store.dispatch(act.setPlayerState({
        timeoutStatus: {
            type: 'delay',
            total,
            past
        }
    }))
})